/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var _config = require("../../config");

var __DEV__ = _config.__DEV__;

var zrUtil = require("static/plugins/js/zrender/lib/core/util");

var graphic = require("../../util/graphic");

var modelUtil = require("../../util/model");

var brushHelper = require("./brushHelper");

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
var each = zrUtil.each;
var indexOf = zrUtil.indexOf;
var curry = zrUtil.curry;
var COORD_CONVERTS = ["dataToPoint", "pointToData"]; // FIXME
// how to genarialize to more coordinate systems.

var INCLUDE_FINDER_MAIN_TYPES = [
    "grid",
    "xAxis",
    "yAxis",
    "geo",
    "graph",
    "polar",
    "radiusAxis",
    "angleAxis",
    "bmap",
];
/**
 * [option in constructor]:
 * {
 *     Index/Id/Name of geo, xAxis, yAxis, grid: See util/model#parseFinder.
 * }
 *
 *
 * [targetInfo]:
 *
 * There can be multiple axes in a single targetInfo. Consider the case
 * of `grid` component, a targetInfo represents a grid which contains one or more
 * cartesian and one or more axes. And consider the case of parallel system,
 * which has multiple axes in a coordinate system.
 * Can be {
 *     panelId: ...,
 *     coordSys: <a representitive cartesian in grid (first cartesian by default)>,
 *     coordSyses: all cartesians.
 *     gridModel: <grid component>
 *     xAxes: correspond to coordSyses on index
 *     yAxes: correspond to coordSyses on index
 * }
 * or {
 *     panelId: ...,
 *     coordSys: <geo coord sys>
 *     coordSyses: [<geo coord sys>]
 *     geoModel: <geo component>
 * }
 *
 *
 * [panelOpt]:
 *
 * Make from targetInfo. Input to BrushController.
 * {
 *     panelId: ...,
 *     rect: ...
 * }
 *
 *
 * [area]:
 *
 * Generated by BrushController or user input.
 * {
 *     panelId: Used to locate coordInfo directly. If user inpput, no panelId.
 *     brushType: determine how to convert to/from coord('rect' or 'polygon' or 'lineX/Y').
 *     Index/Id/Name of geo, xAxis, yAxis, grid: See util/model#parseFinder.
 *     range: pixel range.
 *     coordRange: representitive coord range (the first one of coordRanges).
 *     coordRanges: <Array> coord ranges, used in multiple cartesian in one grid.
 * }
 */

/**
 * @param {Object} option contains Index/Id/Name of xAxis/yAxis/geo/grid
 *        Each can be {number|Array.<number>}. like: {xAxisIndex: [3, 4]}
 * @param {module:echarts/model/Global} ecModel
 * @param {Object} [opt]
 * @param {Array.<string>} [opt.include] include coordinate system types.
 */

function BrushTargetManager(option, ecModel, opt) {
    /**
     * @private
     * @type {Array.<Object>}
     */
    var targetInfoList = (this._targetInfoList = []);
    var info = {};
    var foundCpts = parseFinder(ecModel, option);
    each(targetInfoBuilders, function (builder, type) {
        if (!opt || !opt.include || indexOf(opt.include, type) >= 0) {
            builder(foundCpts, targetInfoList, info);
        }
    });
}

var proto = BrushTargetManager.prototype;

proto.setOutputRanges = function (areas, ecModel) {
    this.matchOutputRanges(
        areas,
        ecModel,
        function (area, coordRange, coordSys) {
            (area.coordRanges || (area.coordRanges = [])).push(coordRange); // area.coordRange is the first of area.coordRanges

            if (!area.coordRange) {
                area.coordRange = coordRange; // In 'category' axis, coord to pixel is not reversible, so we can not
                // rebuild range by coordRange accrately, which may bring trouble when
                // brushing only one item. So we use __rangeOffset to rebuilding range
                // by coordRange. And this it only used in brush component so it is no
                // need to be adapted to coordRanges.

                var result = coordConvert[area.brushType](
                    0,
                    coordSys,
                    coordRange
                );
                area.__rangeOffset = {
                    offset: diffProcessor[area.brushType](
                        result.values,
                        area.range,
                        [1, 1]
                    ),
                    xyMinMax: result.xyMinMax,
                };
            }
        }
    );
};

proto.matchOutputRanges = function (areas, ecModel, cb) {
    each(
        areas,
        function (area) {
            var targetInfo = this.findTargetInfo(area, ecModel);

            if (targetInfo && targetInfo !== true) {
                zrUtil.each(targetInfo.coordSyses, function (coordSys) {
                    var result = coordConvert[area.brushType](
                        1,
                        coordSys,
                        area.range
                    );
                    cb(area, result.values, coordSys, ecModel);
                });
            }
        },
        this
    );
};

proto.setInputRanges = function (areas, ecModel) {
    each(
        areas,
        function (area) {
            var targetInfo = this.findTargetInfo(area, ecModel);
            area.range = area.range || []; // convert coordRange to global range and set panelId.

            if (targetInfo && targetInfo !== true) {
                area.panelId = targetInfo.panelId; // (1) area.range shoule always be calculate from coordRange but does
                // not keep its original value, for the sake of the dataZoom scenario,
                // where area.coordRange remains unchanged but area.range may be changed.
                // (2) Only support converting one coordRange to pixel range in brush
                // component. So do not consider `coordRanges`.
                // (3) About __rangeOffset, see comment above.

                var result = coordConvert[area.brushType](
                    0,
                    targetInfo.coordSys,
                    area.coordRange
                );
                var rangeOffset = area.__rangeOffset;
                area.range = rangeOffset
                    ? diffProcessor[area.brushType](
                          result.values,
                          rangeOffset.offset,
                          getScales(result.xyMinMax, rangeOffset.xyMinMax)
                      )
                    : result.values;
            }
        },
        this
    );
};

proto.makePanelOpts = function (api, getDefaultBrushType) {
    return zrUtil.map(this._targetInfoList, function (targetInfo) {
        var rect = targetInfo.getPanelRect();
        return {
            panelId: targetInfo.panelId,
            defaultBrushType:
                getDefaultBrushType && getDefaultBrushType(targetInfo),
            clipPath: brushHelper.makeRectPanelClipPath(rect),
            isTargetByCursor: brushHelper.makeRectIsTargetByCursor(
                rect,
                api,
                targetInfo.coordSysModel
            ),
            getLinearBrushOtherExtent:
                brushHelper.makeLinearBrushOtherExtent(rect),
        };
    });
};

proto.controlSeries = function (area, seriesModel, ecModel) {
    // Check whether area is bound in coord, and series do not belong to that coord.
    // If do not do this check, some brush (like lineX) will controll all axes.
    var targetInfo = this.findTargetInfo(area, ecModel);
    return (
        targetInfo === true ||
        (targetInfo &&
            indexOf(targetInfo.coordSyses, seriesModel.coordinateSystem) >= 0)
    );
};
/**
 * If return Object, a coord found.
 * If reutrn true, global found.
 * Otherwise nothing found.
 *
 * @param {Object} area
 * @param {Array} targetInfoList
 * @return {Object|boolean}
 */

proto.findTargetInfo = function (area, ecModel) {
    var targetInfoList = this._targetInfoList;
    var foundCpts = parseFinder(ecModel, area);

    for (var i = 0; i < targetInfoList.length; i++) {
        var targetInfo = targetInfoList[i];
        var areaPanelId = area.panelId;

        if (areaPanelId) {
            if (targetInfo.panelId === areaPanelId) {
                return targetInfo;
            }
        } else {
            for (var i = 0; i < targetInfoMatchers.length; i++) {
                if (targetInfoMatchers[i](foundCpts, targetInfo)) {
                    return targetInfo;
                }
            }
        }
    }

    return true;
};

function formatMinMax(minMax) {
    minMax[0] > minMax[1] && minMax.reverse();
    return minMax;
}

function parseFinder(ecModel, option) {
    return modelUtil.parseFinder(ecModel, option, {
        includeMainTypes: INCLUDE_FINDER_MAIN_TYPES,
    });
}

var targetInfoBuilders = {
    grid: function (foundCpts, targetInfoList) {
        var xAxisModels = foundCpts.xAxisModels;
        var yAxisModels = foundCpts.yAxisModels;
        var gridModels = foundCpts.gridModels; // Remove duplicated.

        var gridModelMap = zrUtil.createHashMap();
        var xAxesHas = {};
        var yAxesHas = {};

        if (!xAxisModels && !yAxisModels && !gridModels) {
            return;
        }

        each(xAxisModels, function (axisModel) {
            var gridModel = axisModel.axis.grid.model;
            gridModelMap.set(gridModel.id, gridModel);
            xAxesHas[gridModel.id] = true;
        });
        each(yAxisModels, function (axisModel) {
            var gridModel = axisModel.axis.grid.model;
            gridModelMap.set(gridModel.id, gridModel);
            yAxesHas[gridModel.id] = true;
        });
        each(gridModels, function (gridModel) {
            gridModelMap.set(gridModel.id, gridModel);
            xAxesHas[gridModel.id] = true;
            yAxesHas[gridModel.id] = true;
        });
        gridModelMap.each(function (gridModel) {
            var grid = gridModel.coordinateSystem;
            var cartesians = [];
            each(grid.getCartesians(), function (cartesian, index) {
                if (
                    indexOf(xAxisModels, cartesian.getAxis("x").model) >= 0 ||
                    indexOf(yAxisModels, cartesian.getAxis("y").model) >= 0
                ) {
                    cartesians.push(cartesian);
                }
            });
            targetInfoList.push({
                panelId: "grid--" + gridModel.id,
                gridModel: gridModel,
                coordSysModel: gridModel,
                // Use the first one as the representitive coordSys.
                coordSys: cartesians[0],
                coordSyses: cartesians,
                getPanelRect: panelRectBuilder.grid,
                xAxisDeclared: xAxesHas[gridModel.id],
                yAxisDeclared: yAxesHas[gridModel.id],
            });
        });
    },
    geo: function (foundCpts, targetInfoList) {
        each(foundCpts.geoModels, function (geoModel) {
            var coordSys = geoModel.coordinateSystem;
            targetInfoList.push({
                panelId: "geo--" + geoModel.id,
                geoModel: geoModel,
                coordSysModel: geoModel,
                coordSys: coordSys,
                coordSyses: [coordSys],
                getPanelRect: panelRectBuilder.geo,
            });
        });
    },
};
var targetInfoMatchers = [
    // grid
    function (foundCpts, targetInfo) {
        var xAxisModel = foundCpts.xAxisModel;
        var yAxisModel = foundCpts.yAxisModel;
        var gridModel = foundCpts.gridModel;
        !gridModel && xAxisModel && (gridModel = xAxisModel.axis.grid.model);
        !gridModel && yAxisModel && (gridModel = yAxisModel.axis.grid.model);
        return gridModel && gridModel === targetInfo.gridModel;
    }, // geo
    function (foundCpts, targetInfo) {
        var geoModel = foundCpts.geoModel;
        return geoModel && geoModel === targetInfo.geoModel;
    },
];
var panelRectBuilder = {
    grid: function () {
        // grid is not Transformable.
        return this.coordSys.grid.getRect().clone();
    },
    geo: function () {
        var coordSys = this.coordSys;
        var rect = coordSys.getBoundingRect().clone(); // geo roam and zoom transform

        rect.applyTransform(graphic.getTransform(coordSys));
        return rect;
    },
};
var coordConvert = {
    lineX: curry(axisConvert, 0),
    lineY: curry(axisConvert, 1),
    rect: function (to, coordSys, rangeOrCoordRange) {
        var xminymin = coordSys[COORD_CONVERTS[to]]([
            rangeOrCoordRange[0][0],
            rangeOrCoordRange[1][0],
        ]);
        var xmaxymax = coordSys[COORD_CONVERTS[to]]([
            rangeOrCoordRange[0][1],
            rangeOrCoordRange[1][1],
        ]);
        var values = [
            formatMinMax([xminymin[0], xmaxymax[0]]),
            formatMinMax([xminymin[1], xmaxymax[1]]),
        ];
        return {
            values: values,
            xyMinMax: values,
        };
    },
    polygon: function (to, coordSys, rangeOrCoordRange) {
        var xyMinMax = [
            [Infinity, -Infinity],
            [Infinity, -Infinity],
        ];
        var values = zrUtil.map(rangeOrCoordRange, function (item) {
            var p = coordSys[COORD_CONVERTS[to]](item);
            xyMinMax[0][0] = Math.min(xyMinMax[0][0], p[0]);
            xyMinMax[1][0] = Math.min(xyMinMax[1][0], p[1]);
            xyMinMax[0][1] = Math.max(xyMinMax[0][1], p[0]);
            xyMinMax[1][1] = Math.max(xyMinMax[1][1], p[1]);
            return p;
        });
        return {
            values: values,
            xyMinMax: xyMinMax,
        };
    },
};

function axisConvert(axisNameIndex, to, coordSys, rangeOrCoordRange) {
    var axis = coordSys.getAxis(["x", "y"][axisNameIndex]);
    var values = formatMinMax(
        zrUtil.map([0, 1], function (i) {
            return to
                ? axis.coordToData(axis.toLocalCoord(rangeOrCoordRange[i]))
                : axis.toGlobalCoord(axis.dataToCoord(rangeOrCoordRange[i]));
        })
    );
    var xyMinMax = [];
    xyMinMax[axisNameIndex] = values;
    xyMinMax[1 - axisNameIndex] = [NaN, NaN];
    return {
        values: values,
        xyMinMax: xyMinMax,
    };
}

var diffProcessor = {
    lineX: curry(axisDiffProcessor, 0),
    lineY: curry(axisDiffProcessor, 1),
    rect: function (values, refer, scales) {
        return [
            [
                values[0][0] - scales[0] * refer[0][0],
                values[0][1] - scales[0] * refer[0][1],
            ],
            [
                values[1][0] - scales[1] * refer[1][0],
                values[1][1] - scales[1] * refer[1][1],
            ],
        ];
    },
    polygon: function (values, refer, scales) {
        return zrUtil.map(values, function (item, idx) {
            return [
                item[0] - scales[0] * refer[idx][0],
                item[1] - scales[1] * refer[idx][1],
            ];
        });
    },
};

function axisDiffProcessor(axisNameIndex, values, refer, scales) {
    return [
        values[0] - scales[axisNameIndex] * refer[0],
        values[1] - scales[axisNameIndex] * refer[1],
    ];
} // We have to process scale caused by dataZoom manually,
// although it might be not accurate.

function getScales(xyMinMaxCurr, xyMinMaxOrigin) {
    var sizeCurr = getSize(xyMinMaxCurr);
    var sizeOrigin = getSize(xyMinMaxOrigin);
    var scales = [sizeCurr[0] / sizeOrigin[0], sizeCurr[1] / sizeOrigin[1]];
    isNaN(scales[0]) && (scales[0] = 1);
    isNaN(scales[1]) && (scales[1] = 1);
    return scales;
}

function getSize(xyMinMax) {
    return xyMinMax
        ? [xyMinMax[0][1] - xyMinMax[0][0], xyMinMax[1][1] - xyMinMax[1][0]]
        : [NaN, NaN];
}

var _default = BrushTargetManager;
module.exports = _default;
